/* Copyright(C) 2006-2008 Dave Sexton  *
 * http://www.codeplex.com/aip         *
 * http://www.codeplex.com/aip/license *
 ***************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Configuration.Provider;
using System.Globalization;
using System.ComponentModel;
using System.Collections.Specialized;
using DaveSexton.Web.Controls.Resources;

namespace DaveSexton.Web.Controls
{
	/// <summary>
	/// Provides methods for parsing the string values in an instance of <see cref="NameValueCollection"/> and converts them to other types.
	/// This class cannot be inherited.
	/// </summary>
	/// <include file='examples.xml' path='//example[@id="ProviderHelper"]'/>
	public sealed class ProviderHelper
	{
		#region Private / Protected
		private readonly NameValueCollection config;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="ProviderHelper" /> class.
		/// </summary>
		/// <exception cref="ArgumentNullException">The specified <paramref name="config"/> argument is <see langword="null"/>.</exception>
		/// <param name="config">The collection from which named string values will be read.</param>
		public ProviderHelper(NameValueCollection config)
		{
			if (config == null)
				throw new ArgumentNullException("config");

			this.config = config;
		}
		#endregion

		#region Methods
		/// <summary>
		/// Creates and initializes a new <see cref="ProviderException"/> with the specified <paramref name="innerException"/> and 
		/// formatted message, using the culture of the current thread.
		/// </summary>
		/// <param name="innerException">The inner <see cref="Exception"/>.  This argument may be <see langword="null"/>.</param>
		/// <param name="format">A composite format string.</param>
		/// <param name="arguments">An <see cref="Object">Object[]</see> containing zero or more objects to format</param>
		/// <returns>A new <see cref="ProviderException"/> with the specified <paramref name="innerException"/> and formatted message.</returns>
		public static ProviderException CreateException(Exception innerException, string format, params object[] arguments)
		{
			string message = (arguments == null) ? format : string.Format(CultureInfo.CurrentCulture, format, arguments);

			if (innerException == null)
				return new ProviderException(message);
			else
				return new ProviderException(message, innerException);
		}

		/// <summary>
		/// Converts the value with the specified <paramref name="name"/> into an <see cref="Int32"/>.
		/// </summary>
		/// <remarks>
		/// A <see langword="null"/> or empty string is treated as a <em>missing</em> value.
		/// </remarks>
		/// <exception cref="ProviderException">The value being converted is not valid or overflows <see cref="Int32"/>, or <paramref name="required"/> 
		/// is <see langword="true"/> and a value with the specified <paramref name="name"/> does not exist.</exception>
		/// <param name="name">The name of the value to be converted.</param>
		/// <param name="required">Specifies whether <see cref="ProviderException"/> is thrown if a value with the specified 
		/// <paramref name="name"/> does not exist.</param>
		/// <param name="defaultValue">Indicates the default value if <paramref name="required"/> is <see langword="false"/> and 
		/// a value with the specified <paramref name="name"/> does not exist.</param>
		/// <returns><see cref="Int32"/> that is the converted from the value with the specified <paramref name="name"/> or the 
		/// specified <paramref name="defaultValue"/> if <paramref name="required"/> is <see langword="false"/> and a value with the
		/// specified <paramref name="name"/> does not exist.</returns>
		public int ParseInt32(string name, bool required, int defaultValue)
		{
			string value = config[name];
			config.Remove(name);

			if (!string.IsNullOrEmpty(value))
			{
				try
				{
					return int.Parse(value, CultureInfo.CurrentCulture);
				}
				catch (OverflowException ex)
				{
					throw CreateException(ex, Errors.ProviderHelper_Overflow, name);
				}
				catch (FormatException ex)
				{
					throw CreateException(ex, Errors.ProviderHelper_Format, name);
				}
			}
			else if (required)
				throw CreateException(null, Errors.ProviderHelper_Required, name);
			else
				return defaultValue;
		}

		/// <summary>
		/// Converts the value with the specified <paramref name="name"/> into a <see cref="Single"/>.
		/// </summary>
		/// <remarks>
		/// A <see langword="null"/> or empty string is treated as a <em>missing</em> value.
		/// </remarks>
		/// <exception cref="ProviderException">The value being converted is not valid or overflows <see cref="Single"/>, or <paramref name="required"/> 
		/// is <see langword="true"/> and a value with the specified <paramref name="name"/> does not exist.</exception>
		/// <param name="name">The name of the value to be converted.</param>
		/// <param name="required">Specifies whether <see cref="ProviderException"/> is thrown if a value with the specified 
		/// <paramref name="name"/> does not exist.</param>
		/// <param name="defaultValue">Indicates the default value if <paramref name="required"/> is <see langword="false"/> and 
		/// a value with the specified <paramref name="name"/> does not exist.</param>
		/// <returns><see cref="Single"/> that is the converted from the value with the specified <paramref name="name"/> or the 
		/// specified <paramref name="defaultValue"/> if <paramref name="required"/> is <see langword="false"/> and a value with the
		/// specified <paramref name="name"/> does not exist.</returns>
		public float ParseSingle(string name, bool required, float defaultValue)
		{
			string value = config[name];
			config.Remove(name);

			if (!string.IsNullOrEmpty(value))
			{
				try
				{
					return float.Parse(value, CultureInfo.CurrentCulture);
				}
				catch (OverflowException ex)
				{
					throw CreateException(ex, Errors.ProviderHelper_Overflow, name);
				}
				catch (FormatException ex)
				{
					throw CreateException(ex, Errors.ProviderHelper_Format, name);
				}
			}
			else if (required)
				throw CreateException(null, Errors.ProviderHelper_Required, name);
			else
				return defaultValue;
		}

		/// <summary>
		/// Converts the value with the specified <paramref name="name"/> into a <see cref="Boolean"/>.
		/// </summary>
		/// <remarks>
		/// A <see langword="null"/> or empty string is treated as a <em>missing</em> value.
		/// </remarks>
		/// <exception cref="ProviderException">The value being converted is not a valid <see cref="Boolean"/> or <paramref name="required"/> 
		/// is <see langword="true"/> and a value with the specified <paramref name="name"/> does not exist.</exception>
		/// <param name="name">The name of the value to be converted.</param>
		/// <param name="required">Specifies whether <see cref="ProviderException"/> is thrown if a value with the specified 
		/// <paramref name="name"/> does not exist.</param>
		/// <param name="defaultValue">Indicates the default value if <paramref name="required"/> is <see langword="false"/> and 
		/// a value with the specified <paramref name="name"/> does not exist.</param>
		/// <returns><see cref="Boolean"/> that is the converted from the value with the specified <paramref name="name"/> or the 
		/// specified <paramref name="defaultValue"/> if <paramref name="required"/> is <see langword="false"/> and a value with the
		/// specified <paramref name="name"/> does not exist.</returns>
		public bool ParseBoolean(string name, bool required, bool defaultValue)
		{
			string value = config[name];
			config.Remove(name);

			if (!string.IsNullOrEmpty(value))
			{
				try
				{
					return bool.Parse(value);
				}
				catch (FormatException ex)
				{
					throw CreateException(ex, Errors.ProviderHelper_Format, name);
				}
			}
			else if (required)
				throw CreateException(null, Errors.ProviderHelper_Required, name);
			else
				return defaultValue;
		}

		/// <summary>
		/// Converts the value with the specified <paramref name="name"/> into the type specified by <typeparamref name="TEnum"/>.
		/// </summary>
		/// <remarks>
		/// A <see langword="null"/> or empty string is treated as a <em>missing</em> value.
		/// </remarks>
		/// <exception cref="InvalidOperationException"><typeparamref name="TEnum"/> is not an <see cref="Enum"/> type.</exception>
		/// <exception cref="ProviderException">The value cannot be converted to the type specified by <typeparamref name="TEnum"/> or <paramref name="required"/> 
		/// is <see langword="true"/> and a value with the specified <paramref name="name"/> does not exist.</exception>
		/// <typeparam name="TEnum">The type of <see cref="Enum"/> in which the value being parsed is defined.</typeparam>
		/// <param name="name">The name of the value to be converted.</param>
		/// <param name="required">Specifies whether <see cref="ProviderException"/> is thrown if a value with the specified 
		/// <paramref name="name"/> does not exist.</param>
		/// <param name="defaultValue">Indicates the default value if <paramref name="required"/> is <see langword="false"/> and 
		/// a value with the specified <paramref name="name"/> does not exist.</param>
		/// <returns>A constant defined by the type specified by <typeparamref name="TEnum"/> that is the converted from the value with the specified 
		/// <paramref name="name"/>, or the specified <paramref name="defaultValue"/> if <paramref name="required"/> is <see langword="false"/> and a 
		/// value with the specified <paramref name="name"/> does not exist.</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly")]
		public TEnum ParseEnum<TEnum>(string name, bool required, TEnum defaultValue)
			where TEnum : struct
		{
			if (!typeof(TEnum).IsEnum)
				throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Errors.ProviderHelper_TEnumInvalid, "TEnum"));

			string value = config[name];
			config.Remove(name);

			if (!string.IsNullOrEmpty(value))
			{
				try
				{
					return (TEnum) Enum.Parse(typeof(TEnum), value, true);
				}
				catch (ArgumentException ex)
				{
					throw CreateException(ex, Errors.ProviderHelper_Enum, name);
				}
			}
			else if (required)
				throw CreateException(null, Errors.ProviderHelper_Required, name);
			else
				return defaultValue;
		}

		/// <summary>
		/// Parses the value with the specified <paramref name="name"/> into an <see cref="IList{T}"/> of the type specified 
		/// by <typeparamref name="T"/>, using the specified <paramref name="separators"/>.
		/// </summary>
		/// <remarks>
		/// <para>
		/// The type specified by the generic type argument, <typeparamref name="T"/>, must be associated with a <see cref="TypeConverter"/>
		/// that can convert from <see cref="System.String">System.String</see>; otherwise, <see cref="InvalidOperationException"/> is thrown.
		/// </para>
		/// <para>
		/// A <see langword="null"/> or empty string is treated as a <em>missing</em> value, in which case <see langword="null"/> is returned.
		/// </para>
		/// </remarks>
		/// <exception cref="InvalidOperationException">The type specified by <typeparamref name="T"/> is not associated with a <see cref="TypeConverter"/>
		/// that can convert from <see cref="System.String">System.String</see>.</exception>
		/// <exception cref="ProviderException">An element in the value with the specified <paramref name="name"/> cannot be converted to the type specified 
		/// by <typeparamref name="T"/>.</exception>
		/// <seealso cref="ParseCollection{T}"/>
		/// <typeparam name="T">The type to which each element will be converted.</typeparam>
		/// <param name="name">The name of the value to be converted.</param>
		/// <param name="required">Specifies whether <see cref="ProviderException"/> is thrown if a value with the specified 
		/// <paramref name="name"/> does not exist.</param>
		/// <param name="includeEmptyItems">Specifies whether an empty string should be converted.</param>
		/// <param name="trimItemsBeforeParse">Specifies whether strings are trimmed before being converted.</param>
		/// <param name="separators">A <see cref="Char">Char[]</see> of characters that separate the individual elements in the collection.</param>
		/// <returns><see cref="IList{T}"/> of the type specified by <typeparamref name="T"/> that iterates over the elements converted 
		/// from the value with the specified <paramref name="name"/>, or <see langword="null"/> if a value with the specified <paramref name="name"/>
		/// does not exist.</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
		public IList<T> ParseList<T>(string name, bool includeEmptyItems, bool trimItemsBeforeParse, bool required, params char[] separators)
		{
			string value = config[name];

			if (string.IsNullOrEmpty(value))
				return null;
			else
				return new List<T>(ParseCollection<T>(name, includeEmptyItems, trimItemsBeforeParse, required, separators));
		}

		/// <summary>
		/// Parses the value with the specified <paramref name="name"/> into an <see cref="IEnumerable{T}"/> of the type specified 
		/// by <typeparamref name="T"/>, using the specified <paramref name="separators"/>.
		/// </summary>
		/// <remarks>
		/// <para>
		/// The type specified by the generic type argument, <typeparamref name="T"/>, must be associated with a <see cref="TypeConverter"/>
		/// that can convert from <see cref="System.String">System.String</see>; otherwise, <see cref="InvalidOperationException"/> is thrown.
		/// </para>
		/// <para>
		/// A <see langword="null"/> or empty string is treated as a <em>missing</em> value.
		/// </para>
		/// <para>
		/// See <see cref="ProviderHelper"/> for an example.
		/// </para>
		/// </remarks>
		/// <exception cref="InvalidOperationException">The type specified by <typeparamref name="T"/> is not associated with a <see cref="TypeConverter"/>
		/// that can convert from <see cref="System.String">System.String</see>.</exception>
		/// <exception cref="ProviderException">An element in the value with the specified <paramref name="name"/> cannot be converted to the type specified 
		/// by <typeparamref name="T"/>.</exception>
		/// <seealso cref="ParseList{T}"/>
		/// <typeparam name="T">The type to which each element will be converted.</typeparam>
		/// <param name="name">The name of the value to be converted.</param>
		/// <param name="required">Specifies whether <see cref="ProviderException"/> is thrown if a value with the specified 
		/// <paramref name="name"/> does not exist.</param>
		/// <param name="includeEmptyItems">Specifies whether an empty string should be converted.</param>
		/// <param name="trimItemsBeforeParse">Specifies whether strings are trimmed before being converted.</param>
		/// <param name="separators">A <see cref="Char">Char[]</see> of characters that separate the individual elements in the collection.</param>
		/// <returns><see cref="IEnumerable{T}"/> of the type specified by <typeparamref name="T"/> that iterates over the elements converted 
		/// from the value with the specified <paramref name="name"/>.</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
		public IEnumerable<T> ParseCollection<T>(string name, bool includeEmptyItems, bool trimItemsBeforeParse, bool required, params char[] separators)
		{
			TypeConverter converter = TypeDescriptor.GetConverter(typeof(T));

			if (converter == null || !converter.CanConvertFrom(typeof(string)))
				throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Errors.ProviderHelper_InvalidListItemConverter, name));

			string value = config[name];
			config.Remove(name);

			if (!string.IsNullOrEmpty(value))
			{
				string[] array = value.Split(separators, (includeEmptyItems) ? StringSplitOptions.None : StringSplitOptions.RemoveEmptyEntries);

				foreach (string item in array)
				{
					string itemToConvert;
					T convertedItem;

					if (trimItemsBeforeParse)
					{
						itemToConvert = item.Trim();

						if (!includeEmptyItems && itemToConvert.Length == 0)
							continue;
					}
					else
						itemToConvert = item;

					try
					{
						convertedItem = (T) converter.ConvertFromString(itemToConvert);
					}
					catch (NotSupportedException ex)
					{
						throw CreateException(ex, Errors.ProviderHelper_InvalidListItemConverter, name);
					}
					catch (Exception ex)
					{
						throw CreateException(ex, Errors.ProviderHelper_ItemConversionError, name);
					}

					yield return convertedItem;
				}
			}
			else if (required)
				throw CreateException(null, Errors.ProviderHelper_Required, name);
		}
		#endregion
	}
}
